package AFX::Session;

=head1 NAME

AFX::Session - Session Management for AFX

  close_session               - Close opened session
  close_telnet()              - Close telnet session
  DESTROY()                   - ARF destructor
  execute()                   - Execute command in opened session
  execute_dumb()              - Execute command in dumb session
  execute_telnet()            - Execute command in telnet session
  get_terminal_id()           - Return Terminal's Identifier
  get_terminal_idle()         - Return Terminal's idle
  get_terminal_port()         - Return Terminal's port
  get_terminal_separator()    - Return Terminal's separator
  get_terminal_timeout()      - Return Terminal's timeout
  get_terminal_whoami()       - Return Terminal's whoami
  message_closed()            - Message: Session's connection closed
  message_connected()         - Message: Session's connected
  message_denied()            - Message: Session's permission denied
  message_executing()         - Message: Command is executing
  message_failed_executing()  - Message: Executing command failed
  message_failed_expecting()  - Message: Expecting password, fingerprint or prompt timeout
  message_failed_fork()       - Message: Session spawning failed
  message_failed_session()    - Message: Session not connected
  message_failed_tty()        - Message: Session openning Tty failed
  message_invalid_output()    - Message: Invalid output from terminal
  message_invalid_passwd()    - Message: Invalid password specified.
  message_invalid_prompt()    - Message: Invalid prompt specified.
  message_invalid_terminal()  - Message: Invalid terminal specified
  message_killed()            - Message: Session's killed
  new()                       - Session contructor
  open_session()              - Open session
  open_ssh()                  - Open SSH session
  open_telnet()               - Open Telnet session
  remove_symbols()            - Remove symbols from command output:
  set_logfile()               - Set new logfile name
  spawn()                     - Open Tty for session
  su()                        - Became super user in opened session
  su_telnet()                 - Became super user in telnet session

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2010 Mikhail Volkov

This file is part of AuditFX.

    AuditFX is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    AuditFX is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with AuditFX.  If not, see <http://www.gnu.org/licenses/>.
=cut

use strict;
use Carp;
use Exporter ();

use vars qw($VERSION);
my @ISA = qw(Exporter);
$VERSION = '1.0';

##############################################################################
# Globals
#

use POSIX qw(strftime);
use Net::Telnet qw(TELNET_IAC TELNET_SB TELNET_SE TELOPT_TTYPE);

## Define terminal identifiers
my $terminal_auto   = "auto";
my $terminal_dumb   = "dumb";
my $terminal_telnet = "telnet";
my $terminal_ssh    = "ssh";

##############################################################################
#
# close_session()
#
# Close opened session
#
# Return:
# - 1         - session closed successfully
# - 0         - error happends
#
# Arguments expected are:
# - self      - Object reference

sub close_session {
    my ($self) = @_;

## Close session as telnet
    my $closed = $self->close_telnet();

## If session closed
    if ($closed) {

## Print Session closed message
        $self->message_closed();
    }

## If pid specified and process exists
    if ( $self->{_pid} and kill( 17, $self->{_pid} ) ) {

## Kill process if exists
        kill( 9, $self->{_pid} );

## Print Session killed message
        $self->message_killed();
    }

## Set new logfile's name, if session will be reopened
    $self->set_logfile();

## If session not opened
    if ( !$self->{_session} ) {

## Remove session from ARF
        $self->{ARF}->remove_session( $self->{id} );

## Return closed status
        return 1;
    }

## Return Error
    return 0;

} ## end sub close_session

##############################################################################
#
# close_telnet()
#
# Close telnet session
#
# Return:
# - 1       - instance closed successfully
# - 0       - error happends
#
# Arguments expected are:
# - self    - Object reference

sub close_telnet {
    my ($self) = @_;

## Return Error, if no session
    return 0 if !$self->{_session};

## Clean buffer
    $self->{_session}->buffer_empty();

## Do it some times
    for ( my $i = 1 ; $i <= 3 ; $i++ ) {

## Print exit
        $self->{_session}->print("exit");

## Wait for closed or NN seconds tag
        my ( $input, $prompt ) = $self->{_session}->waitfor(
            Match   => '/(closed|' . $self->{prompt} . ')/',
            timeout => $self->get_terminal_timeout() / 6
        );

## Exit if exited normally
        last if $prompt !~ /$self->{prompt}/i;
    }

## Close session
    $self->{_session} = 0 if $self->{_session}->close();

## Return Closed, if no session
    return 1 if !$self->{_session};

} ## end sub close_telnet

##############################################################################
#
# DESTROY()
#
# ARF destructor
#
# Arguments expected are:
# - self         - Object reference

sub DESTROY {
    my ($self) = @_;

## Print debug message
    print "DEBUG::Object Session " . $self->{id} . " destroyed\n"
      if $self->{ARF}->{Process}->is_debug();

## Return Ok
    return 1;

} ## end sub DESTROY

##############################################################################
#
# execute()
#
# Execute command in opened session
#
# Return:
# - output       - command executed successfully
#
# Arguments expected are:
# - self         - Object reference
# - cmd          - Command Hash
# - step_id      - Step's Identifier

sub execute {
    my ( $self, $cmd, $step_id ) = @_;

## Change timeout to default if not specified
    if ( !$cmd->{timeout} ) {
        $cmd->{timeout} = $self->get_terminal_timeout();
    }

## Set empty expected end if not defined
    if ( !$cmd->{end} ) {
        $cmd->{end} = "";
    }

## Set empty expected password if not defined
    if ( !$cmd->{passwd} ) {
        $cmd->{passwd} = "";
    }

## Get idle time
    my $session_idle = time() - $self->{_last};

## Check session for IDLE
    if ( $self->{_session} and $session_idle > $self->get_terminal_idle() ) {

## Print Session idle message
        $self->message_idle($session_idle);

## Get prompt
        my $node_prompt =
          $self->{ARF}->get_node_prompt( $self->{node}, $self->{login} );

## Clean buffer
        $self->{_session}->buffer_empty();

## Print nothing
        $self->{_session}->print("");

## Wait for end tag
        my ( $content, $prompt ) = $self->{_session}->waitfor(
            Match   => "/$node_prompt->{content}/",
            Timeout => $cmd->{timeout} / 6
        );

## if prompt not received
        if ( !$prompt ) {

## Close session
            $self->close_session();

## Add session into ARF
            $self->{ARF}->add_session($self);
        }
    }

## If session not available and no errors
    if ( !$self->{_session} and !$self->{_error} ) {

## Try to open session
        $self->open_session();
    }

## If session not available, return Error
    if ( !$self->{_session} ) {

## Return dumb message
        return $self->{_error} if $self->get_terminal_id() =~ /$terminal_dumb/i;

## Print failed session message
        $self->message_failed_session( $self->{_error} );

## Return nothing
        return "";
    }

## Print Session executing message
    $self->message_executing( $cmd->{content}, $cmd->{timeout} )
      if $self->{ARF}->{Process}->is_debug();

## Execute as dumb
    if ( $self->get_terminal_id() =~ /$terminal_dumb/i ) {
        return $self->execute_dumb( $cmd, $step_id );
    }

## Execute as telnet or ssh
    if ( $self->get_terminal_id() =~ /($terminal_telnet|$terminal_ssh)/i ) {
        return $self->execute_telnet( $cmd, $step_id );
    }

## Invalid terminal specified
    return $self->message_invalid_terminal();

} ## end sub execute

##############################################################################
#
# execute_dumb()
#
# Execute command in dumb session
#
# Return:
# - command output  - command executed successfully
# - error           - error happends
#
# Arguments expected are:
# - self            - Object referece
# - cmd             - Command Hash
# - step_id         - Step's Identifier

sub execute_dumb {
    my ( $self, $cmd, $step_id ) = @_;

## Clean buffer
    $self->{_session}->buffer_empty();

## Get prompt
    my $node_prompt =
      $self->{ARF}->get_node_prompt( $self->{node}, $self->{login} );

## If command's end not specified
    if ( !$cmd->{end} or $cmd->{end} eq "" ) {
        $cmd->{end} = $node_prompt->{content};
    }

## Execute command
    $self->{_session}->print( $cmd->{content} );

## Wait for ended tag
    my ( $content, $prompt ) =
      $self->{_session}
      ->waitfor( Match => "/$cmd->{end}/", Timeout => $cmd->{timeout} );

## Define latest prompt
    my $lastline = $self->{_session}->lastline if $self->{_session};

## If end tag not received
    if ( !$prompt ) {

## Try to get end_tag, maybe command required input
        for ( my $i = 1 ; $i < 6 ; $i++ ) {

## Next if start
            last if $prompt or !$self->{_session};

## Print end tag once more
            $self->{_session}->print("");

## Wait for end tag
            ( $content, $prompt ) = $self->{_session}->waitfor(
                Match   => "/$node_prompt->{content}/",
                Timeout => $cmd->{timeout} / 6
            );

## Add latest line
            $lastline .= $content . "\n" . $self->{_session}->lastline
              if $self->{_session};
        }

## Close session if no end received
        $self->close_session() if !$prompt;

## Print Session invalid output message
        return $lastline . "\n" . $self->message_invalid_output($step_id);
    }

## If error happends, return it
    my $errmsg = $self->{_session}->errmsg;
    return $self->message_failed_executing( $cmd->{content}, $errmsg )
      if $errmsg;

## Set last command time
    $self->{_last} = time();

## Return normal output
    return $self->remove_symbols($content);

} ## end sub execute_dumb

##############################################################################
#
# execute_telnet()
#
# Execute command in telnet session
#
# Return:
# - command output  - command executed successfully
# - error           - error happends
#
# Arguments expected are:
# - self            - Object reference
# - cmd             - Command Hash
# - step_id         - Step's Identifier

sub execute_telnet {
    my ( $self, $cmd, $step_id ) = @_;

## Clean buffer
    $self->{_session}->buffer_empty();

## Set tags
    my $start_tag = "__START__" . time();
    my $end_tag   = "__END__" . time();

## Set prompts
    my $start_prompt = "/\n$start_tag/";
    my $end_prompt   = "/$end_tag\n/";

## Set end_prompt if expected end specified
    if ( $cmd->{end} ) {
        $end_prompt = "/$cmd->{end}/";
    }

## Get separator
    my $separator = $self->get_terminal_separator();

## Execute command
    $self->{_session}->print( "echo $start_tag"
          . "$separator$cmd->{content}$separator"
          . "echo $end_tag" );

## Wait for started tag
    my $start =
      $self->{_session}
      ->waitfor( Match => $start_prompt, Timeout => $cmd->{timeout} / 2 );

## If start tag not received
    if ( !$start ) {

## Try to get start_tag
        for ( my $i = 1 ; $i < 6 ; $i++ ) {

## Next if start prompt received
            last if $start or !$self->{_session};

## Print start tag once more
            $self->{_session}->print("echo $start_tag");

## Wait for start tag
            $start = $self->{_session}->waitfor(
                Match   => $start_prompt,
                Timeout => $cmd->{timeout} / 6
            );
        }

## Close session if no start received
        $self->close_session() if !$start;

## Print Session invalid output message
        return $self->message_invalid_output($step_id);
    }

## If password specified
    if ( $cmd->{passwd} ) {

## Wait for password tag
        my ( $content, $prompt ) = $self->{_session}->waitfor(
            Match   => '/password:\s*?$/i',
            Timeout => $cmd->{timeout} / 6
        );

## Enter password
        $self->{_session}->print( $cmd->{passwd} ) if $prompt;
    }

## Wait for ended tag
    my ( $content, $prompt ) =
      $self->{_session}
      ->waitfor( Match => $end_prompt, Timeout => $cmd->{timeout} );

## If lastline is fingerprint and required YES
    if (   !$prompt
        and $self->{_session}
        and $self->{_session}->lastline =~ /(fingerprint|connecting)/i )
    {

## Enter yes
        $self->{_session}->print("yes");

## If password specified
        if ( $cmd->{passwd} ) {

## Wait for password tag
            my ( $content, $prompt ) = $self->{_session}->waitfor(
                Match   => '/password:\s*?$/i',
                Timeout => $cmd->{timeout} / 6
            );

## Enter password
            $self->{_session}->print( $cmd->{passwd} ) if $prompt;
        }

## Wait for ended tag
        ( $content, $prompt ) =
          $self->{_session}
          ->waitfor( Match => $end_prompt, Timeout => $cmd->{timeout} );
    }

## Define latest prompt
    my $lastline = $self->{_session}->lastline if $self->{_session};

## If end tag not received
    if ( !$prompt ) {

## Try to get end_tag, maybe command required input
        for ( my $i = 1 ; $i < 6 ; $i++ ) {

## Next if prompt received
            last if $prompt or !$self->{_session};

## Print end tag once more
            $self->{_session}->print("echo $end_tag");

## Wait for end tag
            ( $content, $prompt ) = $self->{_session}->waitfor(
                Match   => $end_prompt,
                Timeout => $cmd->{timeout} / 6
            );

## Add latest line
            $lastline .= $content . "\n" . $self->{_session}->lastline
              if $self->{_session};
        }

## Close session if no end received
        $self->close_session() if !$prompt;

## Print Session invalid output message
        return $lastline . "\n" . $self->message_invalid_output($step_id);
    }

## If error happends, return it
    my $errmsg = $self->{_session}->errmsg;
    return $self->message_failed_executing( $cmd->{content}, $errmsg )
      if $errmsg;

## Set last command time
    $self->{_last} = time();

## Return normal output
    return $self->remove_symbols($content);

} ## end sub execute_telnet

##############################################################################
#
# get_terminal_id()
#
# Return Terminal's Identifier
#
# Return identifier
# - 0       - otherwise
#
# Arguments expected are:
# - self    - Object reference

sub get_terminal_id {
    my ($self) = @_;

## Return error, if no terminal exist
    return 0 if !$self->{_terminal} or !$self->{_terminal}->{id};

## Return Identifier
    return $self->{_terminal}->{id};

} ## end sub get_terminal_id

##############################################################################
#
# get_terminal_idle()
#
# Return Terminal's idle
#
# Return:
# - idle
#
# Arguments expected are:
# - self    - Object reference

sub get_terminal_idle {
    my ($self) = @_;

## Return timeout
    return $self->{_terminal}->{idle};

} ## end sub get_terminal_idle

##############################################################################
#
# get_terminal_port()
#
# Return Terminal's port
#
# Return:
# - port
#
# Arguments expected are:
# - self    - Object reference

sub get_terminal_port {
    my ($self) = @_;

## Return Port
    return $self->{_terminal}->{port};

} ## end sub get_terminal_port

##############################################################################
#
# get_terminal_separator()
#
# Return Terminal's separator
#
# Return separator
# - ;       - if terminal separator not specified
#
# Arguments expected are:
# - self    - Object reference

sub get_terminal_separator {
    my ($self) = @_;

## Return ;, if no separator specified
    return ";" if !$self->{_terminal}->{separator};

## Return Separator
    return $self->{_terminal}->{separator}->[0]->{content};

} ## end sub get_terminal_separator

##############################################################################
#
# get_terminal_timeout()
#
# Return Terminal's timeout
#
# Return:
# - timeout
#
# Arguments expected are:
# - self    - Object reference

sub get_terminal_timeout {
    my ($self) = @_;

## Return timeout
    return $self->{_terminal}->{timeout};

} ## end sub get_terminal_timeout

##############################################################################
#
# get_terminal_whoami()
#
# Return Terminal's whoami
#
# Return whoami
# - 0       - if no whoami specified
#
# Arguments expected are:
# - self    - Object reference

sub get_terminal_whoami {
    my ($self) = @_;

## Return Error, if no whoami
    return 0 if !$self->{_terminal}->{whoami};

## Return Whoami
    return $self->{_terminal}->{whoami}->[0]->{content};

} ## end sub get_terminal_whoami

##############################################################################
#
# message_closed()
#
# Message: Session's connection closed
#
# Return:
# - logged message
#
# Arguments expected are:
# - self     - Object reference

sub message_closed {
    my ($self) = @_;

## Get time
    my $now = localtime;

## Set message
    my $message = "$now $self->{id}: connection closed";

## Return message
    return $self->{ARF}->{Process}->{ACF}->logging( $message, 0, 'session' );

} ## end sub message_closed

##############################################################################
#
# message_connected()
#
# Message: Session's connected
#
# Return:
# - logged message
#
# Arguments expected are:
# - self     - Object reference

sub message_connected {
    my ($self) = @_;

## Get time
    my $now = localtime;

## Set message
    my $message = "$now $self->{id}: connected successfully";

## Return message
    return $self->{ARF}->{Process}->{ACF}->logging( $message, 0, 'session' );

} ## end sub message_connected

##############################################################################
#
# message_denied()
#
# Message: Session's permission denied
#
# Return:
# - logged message
#
# Arguments expected are:
# - self     - Object reference

sub message_denied {
    my ($self) = @_;

## Set message
    my $message = "$self->{id}: permission denied or connection refused";

## Return message
    return $self->{ARF}->{Process}->{ACF}->logging( $message, 0, 'session' );

} ## end sub message_denied

##############################################################################
#
# message_executing()
#
# Message: Command is executing
#
# Return:
# - logged message
#
# Arguments expected are:
# - self        - Object reference
# - cmd         - Command
# - timeout     - Command's Timeout

sub message_executing {
    my ( $self, $cmd, $timeout ) = @_;

## Get time
    my $now = localtime;

## Set message
    my $message = "$now $self->{id}: ";
    $message .= "command '$cmd' (timeout $timeout) is executing";

## Return message
    return $self->{ARF}->{Process}->{ACF}->logging( $message, 0, 'session' );

} ## end sub message_executing

##############################################################################
#
# message_idle()
#
# Message: Session in idle state
#
# Return:
# - logged message
#
# Arguments expected are:
# - self        - Object reference
# - idle        - Session's Idle

sub message_idle {
    my ( $self, $idle ) = @_;

## Get time
    my $now = localtime;

## Set message
    my $message = "$now $self->{id}: idle for $idle second(s)";

## Return message
    return $self->{ARF}->{Process}->{ACF}->logging( $message, 0, 'session' );

} ## end sub message_idle

##############################################################################
#
# message_failed_executing()
#
# Message: Executing command failed
#
# Return:
# - logged message
#
# Arguments expected are:
# - self        - Object reference
# - cmd         - Command
# - error       - Error happend

sub message_failed_executing {
    my ( $self, $cmd, $error ) = @_;

## Set message
    my $message = "$self->{id}: executing command '$cmd' failed.";
    $message .= "\nError: $error" if $error;

## Return Message, if error already added into ARF
    return $message if !$self->{ARF}->add_error($message);

## Return message
    return $self->{ARF}->{Process}->{ACF}->logging( $message, 1, 'session' );

} ## end sub message_failed_executing

##############################################################################
#
# message_failed_expecting()
#
# Message: Expecting password, fingerprint or prompt timeout
#
# Return:
# - logged message
#
# Arguments expected are:
# - self        - Object reference

sub message_failed_expecting {
    my ($self) = @_;

## Set message
    my $message =
      "$self->{id}: expecting password, fingerprint or prompt timeout";

## Return dumb message
    return $message if $self->get_terminal_id() =~ /$terminal_dumb/i;

## Return Message, if error already added into ARF
    return $message if !$self->{ARF}->add_error($message);

## Return message
    return $self->{ARF}->{Process}->{ACF}->logging( $message, 1, 'session' );

} ## end sub message_failed_expecting

##############################################################################
#
# message_failed_fork()
#
# Message: Session spawning failed
#
# Return:
# - logged message
#
# Arguments expected are:
# - self        - Object reference

sub message_failed_fork {
    my ($self) = @_;

## Set message
    my $message = "$self->{id}: program spawning failed";

## Return Message, if error already added into ARF
    return $message if !$self->{ARF}->add_error($message);

## Return message
    return $self->{ARF}->{Process}->{ACF}->logging( $message, 1, 'session' );

} ## end sub message_failed_fork

##############################################################################
#
# message_failed_session()
#
# Message: Session not connected
#
# Return logged message
#
# Arguments expected are:
# - self        - Object reference

sub message_failed_session {
    my ($self) = @_;

## Set message
    my $message = "$self->{id}: not connected.";
    $message .= "\nError: $self->{_error}" if $self->{_error};

## Return Message, if error already added into ARF
    return $message if !$self->{ARF}->add_error($message);

## Return message
    return $self->{ARF}->{Process}->{ACF}->logging( $message, 1, 'session' );

} ## end sub message_failed_session

##############################################################################
#
# message_failed_tty()
#
# Message: Session openning Tty failed
#
# Return:
# - logged message
#
# Arguments expected are:
# - self         - Object reference

sub message_failed_tty {
    my ($self) = @_;

## Set message
    my $message = "$self->{id}: openning tty failed";

## Return dumb message
    return $message if $self->get_terminal_id() =~ /$terminal_dumb/i;

## Return Message, if error already added into ARF
    return $message if !$self->{ARF}->add_error($message);

## Return message
    return $self->{ARF}->{Process}->{ACF}->logging( $message, 1, 'session' );

} ## end sub message_failed_tty

##############################################################################
#
# message_invalid_output()
#
# Message: Invalid output from terminal
#
# Return logged message
#
# Arguments expected are:
# - self       - Object reference
# - step_id    - Step's Identifier

sub message_invalid_output {
    my ( $self, $step_id ) = @_;

## Set message
    my $message = "$self->{id}: timeout occurs while step $step_id execution";

## Return Message, if error already added into ARF
    return $message if !$self->{ARF}->add_error($message);

## Return message
    return $self->{ARF}->{Process}->{ACF}->logging( $message, 1, 'session' );

} ## end sub message_invalid_output

##############################################################################
#
# message_invalid_passwd()
#
# Message: Invalid password specified.
#
# Return logged message
#
# Arguments expected are:
# - self        - Object reference

sub message_invalid_passwd {
    my ($self) = @_;

## Set message
    my $message = "$self->{id}: can't connect using default passwords.";
    $message .= " Check connection or add passwords.";

## Return dumb message
    return $message if $self->get_terminal_id() =~ /$terminal_dumb/i;

## Return Message, if error already added into ARF
    return $message if !$self->{ARF}->add_error($message);

## Return message
    return $self->{ARF}->{Process}->{ACF}->logging( $message, 1, 'session' );

} ## end sub message_invalid_passwd

##############################################################################
#
# message_invalid_prompt()
#
# Message: Invalid prompt specified.
#
# Return logged message
#
# Arguments expected are:
# - self        - Object reference

sub message_invalid_prompt {
    my ($self) = @_;

## Set message
    my $message = "$self->{id}: prompt for $self->{login} not found.";

## Return Message, if error already added into ARF
    return $message if !$self->{ARF}->add_error($message);

## Return message
    return $self->{ARF}->{Process}->{ACF}->logging( $message, 1, 'session' );

} ## end sub message_invalid_prompt

##############################################################################
#
# message_invalid_terminal()
#
# Message: Invalid terminal specified
#
# Return logged message
#
# Arguments expected are:
# - self      - Object reference

sub message_invalid_terminal {
    my ($self) = @_;

## Set message
    my $message = "$self->{id}: invalid terminal specified";

## Return Message, if error already added into ARF
    return $message if !$self->{ARF}->add_error($message);

## Return message
    return $self->{ARF}->{Process}->{ACF}->logging( $message, 1, 'session' );

} ## end sub message_invalid_terminal

##############################################################################
#
# message_killed()
#
# Message: Session's killed
#
# Return:
# - logged message
#
# Arguments expected are:
# - self     - Object reference

sub message_killed {
    my ($self) = @_;

## Get time
    my $now = localtime;

## Set message
    my $message = "$now $self->{id}: killed successfully";

## Return message
    return $self->{ARF}->{Process}->{ACF}->logging( $message, 0, 'session' );

} ## end sub message_killed

##############################################################################
#
# new()
#
# Session contructor
#
# Arguments expected are:
# - ARF's Object
# - Login
# - Node hash

sub new {

## Get package name
    my $type = shift;

## Define object's hash
    my $self = { ARF => shift, login => shift, node => shift, rooted => 0 };

## Set Session identifier
    $self->{id} = $self->{login} . "@" . $self->{node}->{host};

## Check session already opened
    my $session = $self->{ARF}->get_session( $self->{id} );

## If session already opened, return it
    return $session if $session;

## Add session into ARF
    $self->{ARF}->add_session($self);

## Get first terminal in node
    $self->{_terminal} =
      $self->{ARF}->{Process}->{ASF}->get_node_terminal( $self->{node} );

## If terminal invalid, return Error
    if ( !$self->{_terminal} or !$self->{_terminal}->{id} ) {

## Print invalid terminal message
        $self->message_invalid_terminal();

## Return error
        return 0;
    }

## Define empty root
    $self->{_root} = 0;

## If root specified, set it
    $self->{_root} = $self->{ARF}->get_machine_root( $self->{node}->{host} )
      if $self->{ARF}->get_machine_root( $self->{node}->{host} );

## Set empty session
    $self->{_session} = 0;

## Set no error
    $self->{_error} = 0;

## Set no pid
    $self->{_pid} = 0;

## Set last command time
    $self->{_last} = time();

## Set empty logfile
    $self->{logfile} = "";

## If debug mode or scenario specified
    if ( $self->{ARF}->{Process}->is_debug() or $self->{ARF}->{scenario} ) {

## Set logfile's name
        set_logfile($self);
    }

## Print debug message
    print "DEBUG::Object Session " . $self->{id} . " created\n"
      if $self->{ARF}->{Process}->is_debug();

## Return object
    return bless $self, $type;

} ## end sub new

##############################################################################
#
# open_session()
#
# Open session
#
# Return:
# - 1             - session opened successfully
# - 0             - error happends
#
# Arguments expected are:
# - self          - Object reference

sub open_session {
    my ($self) = @_;

## Return Error, if error already happend
    return 0 if $self->{_error};

## Set passwd
    my $passwd =
      $self->{ARF}->decrypt_passwd( $self->{node}->{host}, $self->{login} );

## If password not specified
    if ( !$passwd or !$passwd->{content} ) {

## Print invalid password message
        $self->{_error} = $self->message_invalid_passwd();

## Return Error
        return 0;
    }

## Set prompt
    my $node_prompt =
      $self->{ARF}->get_node_prompt( $self->{node}, $self->{login} );

## If prompt not specified
    if ( !$node_prompt or !$node_prompt->{content} ) {

## Print invalid password message
        $self->{_error} = $self->message_invalid_prompt();

## Return Error
        return 0;
    }

## Set prompt as property
    $self->{prompt} = $node_prompt->{content};

## Define terminal_id
    my $terminal_id = $self->get_terminal_id();

## Open dumb or auto session
    if (   $terminal_id =~ /$terminal_dumb/i
        or $terminal_id =~ /$terminal_auto/i )
    {

## Parse terminal from ACF
        my $default_terminal =
          $self->{ARF}->{Process}->{ACF}->get_terminal('telnet');

## Update terminal Port
        if ( $default_terminal and $default_terminal->{port} ) {

## Set id and port as ssh
            $self->{_terminal}->{id}   = $default_terminal->{id};
            $self->{_terminal}->{port} = $default_terminal->{port};
        }

## Open as telnet session
        $self->open_telnet( $passwd->{content}, $self->{prompt} );

## If error within telnet occured, try to open as ssh
        if ( $self->{_error} ) {

## Skip error
            $self->{_error} = "";

## Parse terminal from ACF
            my $default_terminal =
              $self->{ARF}->{Process}->{ACF}->get_terminal('ssh');

## Update terminal Port
            if ( $default_terminal and $default_terminal->{port} ) {

## Set id and port as ssh
                $self->{_terminal}->{id}   = $default_terminal->{id};
                $self->{_terminal}->{port} = $default_terminal->{port};
            }

## Open as ssh session
            $self->open_ssh( $passwd->{content}, $self->{prompt} );
        }
    }

## If it's dumb or non-connected auto
    if ( $terminal_id =~ /$terminal_dumb/i
        or ( $terminal_id =~ /$terminal_auto/i and $self->{_error} ) )
    {

## Return terminal's id
        $self->{_terminal}->{id} = $terminal_id;
    }

## Open telnet session
    elsif ( $self->get_terminal_id() =~ /$terminal_telnet/i ) {
        $self->open_telnet( $passwd->{content}, $self->{prompt} );
    }

## Open ssh session
    elsif ( $self->get_terminal_id() =~ /$terminal_ssh/i ) {
        $self->open_ssh( $passwd->{content}, $self->{prompt} );
    }

## Change to specified user and becoming root after
    if (    $self->{_error}
        and !$self->{rooted}
        and $self->{_root}
        and $self->{login} =~ /$self->{_root}->{login}/i )
    {

## Skip error
        $self->{_error} = "";

## Set login before su
        $self->{login} = $self->{_root}->{content};

## Open session
        $self->open_session();

## Set rooted flag
        $self->{rooted} = 1;
    }

## If error happend, return error
    return 0 if $self->{_error};

## Becoming root if have rooted before
    if ( $self->{rooted} ) {

## Redefine login for su
        $self->{login} = $self->{_root}->{login};

## Became super user
        $self->su();
    }

## Check session connection using whoami command
    if ( $self->get_terminal_whoami() ) {

## Define command hash
        my $cmd = { content => $self->get_terminal_whoami() };

## Execute whoami command
        my $whoami = $self->execute($cmd);

## Exception: Executing command as login on node failed
        if ( $whoami !~ /^$self->{login}/ ) {

## Close Session
            $self->close_session();

## Set error
            $self->{_error} =
              $self->message_failed_executing( $self->get_terminal_whoami() );

## Return Error
            return 0;
        }
    }
    else {

## Get timeout
        my $timeout = $self->get_terminal_timeout();

## Clean buffer
        $self->{_session}->buffer_empty();

## Print nothing
        $self->{_session}->print("");

## Wait for end tag
        my ( $content, $prompt ) = $self->{_session}->waitfor(
            Match   => "/$self->{prompt}/",
            Timeout => $timeout / 2
        );

## Exception: Expecting prompt failed
        if ( !$prompt ) {

## Close Session
            $self->close_session();

## Set error
            $self->{_error} = $self->message_failed_expecting();

## Return Error
            return 0;
        }
    }

## If session opened
    if ( $self->{_session} and !$self->{_error} ) {

## Print Session connected message
        $self->message_connected();

## Return Ok
        return 1;
    }

## Set error if invalid terminal specified
    $self->{_error} = $self->message_invalid_terminal();

## Return Error
    return 0;

} ## end sub open_session

##############################################################################
#
# open_ssh()
#
# Open SSH session
#
# Return:
# - 1              - session opened successfully
# - 0              - error happends
#
# Arguments expected are:
# - self           - Object reference
# - passwd         - Login's password
# - prompt         - Login's prompt

sub open_ssh {
    my ( $self, $passwd, $prompt ) = @_;

## Start ssh program.
    my ( $pty, $pid ) = &spawn( $self,
            "ssh -l "
          . $self->{login} . " -p "
          . $self->get_terminal_port() . " "
          . $self->{node}->{ip} );

## If error happends, return it
    if ( !$pty ) {

## Set error message
        $self->{_error} = $self->message_failed_tty();

## Return error
        return 0;
    }

## Set pid if defined
    $self->{_pid} = $pid if $pid;

## Get timeout
    my $timeout = $self->get_terminal_timeout();

## Create new Telnet instance
    my $t = new Net::Telnet(
        Timeout                 => $timeout,
        Errmode                 => 'return',
        Fhopen                  => $pty,
        Telnetmode              => 0,
        Cmd_remove_mode         => 1,
        Output_record_separator => "\r"
    );

## Set Debugging level
    if ( $self->{logfile} ) {
        $t->input_log( $self->{logfile} );
    }

## Set prompt
    $t->prompt("/$prompt/i");

## Wait for password
    my ( $input, $enter_pass ) = $t->waitfor(
        Match => '/(password: ?$|' 
          . $prompt
          . '|fingerprint|Connection refused)/i',
        Timeout => $timeout / 2
    );

## If fingerprint required
    if ( $enter_pass =~ /fingerprint/i ) {

## Enter yes
        $t->print("yes");

## Wait for password
        ( $input, $enter_pass ) = $t->waitfor(
            Match   => '/(password: ?$|' . $prompt . ')/i',
            Timeout => $timeout / 2
        );
    }

## If no password required
    if ( $enter_pass =~ /$prompt/i ) {

## If connected and login return normal
        $self->{_session} = $t;

## Return Ok
        return 1;
    }

## If Connection Refused
    if ( $enter_pass =~ /Connection refused/i ) {

## Set Error
        $self->{_error} = $self->message_denied();

## Return Error
        return $self->{_error};
    }

## If session not connected
    if ( !$enter_pass ) {

## Set Error
        $self->{_error} = $self->message_failed_expecting();

## Return Error
        return $self->{_error};
    }

## Enter password
    $t->print($passwd);

## Wait for prompt
    ( $input, $enter_pass ) = $t->waitfor(
        Match   => "/($prompt|Permission denied)/i",
        Timeout => $timeout / 2
    );

## If Permission denied
    if ( $enter_pass =~ /Permission denied/i ) {

## Set Error
        $self->{_error} = $self->message_denied();

## Return Error
        return $self->{_error};
    }

## If error happends, return error
    $self->{_error} = $t->errmsg;
    return 0 if $self->{_error};

## Set session
    $self->{_session} = $t;

## Return Ok
    return 1;

} ## end sub open_ssh

##############################################################################
#
# open_telnet()
#
# Open Telnet session
#
# Return:
# - 1              - session opened successfully
# - 0              - error happends
#
# Arguments expected are:
# - self           - Object reference
# - passwd         - Login's password
# - prompt         - Login's prompt

sub open_telnet {
    my ( $self, $passwd, $prompt ) = @_;

## Get timeout
    my $timeout = $self->get_terminal_timeout();

## Create new Telnet instance
    my $t = new Net::Telnet(
        Timeout => $timeout,
        Errmode => 'return'
    );

## Set Debugging level
    if ( $self->{logfile} ) {
        $t->input_log( $self->{logfile} );
    }

## Open port
    $t->open(
        Host => $self->{node}->{ip},
        Port => $self->get_terminal_port()
    );

## If error happends, return error
    $self->{_error} = $t->errmsg;
    return 0 if $self->{_error};

## Set prompt
    $t->prompt("/$prompt/i");

## Login and wait for prompt
    $t->login( Name => $self->{login}, Password => $passwd );

## If error happends, return error
    $self->{_error} = $t->errmsg;
    return 0 if $self->{_error};

## If error happends, return error
    $self->{_error} = $t->errmsg;
    return 0 if $self->{_error};

## Set session
    $self->{_session} = $t;

## Return Ok
    return 1;

} ## end sub open_telnet

##############################################################################
#
# remove_symbols()
#
# Remove symbols from command output:
# - spaces and new lines at the start and end of the output
# - carrier-return at all
# - backspace
#
# Return:
# - better content
#
# Arguments expected are:
# - self      - Object reference
# - content   - command's output

sub remove_symbols {
    my ( $self, $content ) = @_;

## If content is empty, return empty
    return "" if !$content;

## Remove linux color ASCII symbols
    $content =~ s/(\033\[60G)|(\033\[0;31m)|(\033\[0;32m)|(\033\[0;39m)/ /g;

## Remove non-interested ASCII symbols
    $content =~ s/[^\x0A\x20-\x7E]/ /g;

## Remove spaces and new lines in the start
    $content =~ s/^[\s\n]+//g;

## Remove spaces at end of each line
    $content =~ s/\x20+\n/\n/g;

## Remove spaces and new lines at the end
    $content =~ s/[\s\n]+$//g;

## Return content
    return $content;

} ## end sub remove_symbols

##############################################################################
#
# set_logfile()
#
# Set new logfile name
#
# Return:
# - 1, if logfile set
#
# Arguments expected are:
# - self      - Object reference

sub set_logfile {
    my ($self) = @_;

## Set logname
    $self->{logfile} =
      strftime( "%y%m%d_%H%M%S", localtime ) . "_" . $self->{id} . ".log";

## Set logdir
    $self->{logdir} =
        $self->{ARF}->{Process}->{ACF}->get_directory('log') . "/"
      . $self->{ARF}->get_site() . "/";

## Make directory unless it already exists
    mkdir( $self->{logdir} ) unless -d $self->{logdir};

## Set full path to logfile
    $self->{logfile} = $self->{logdir} . $self->{logfile};

## Return Ok
    return 1;

} ## end sub set_logfile

##############################################################################
#
# spawn()
#
# Open Tty for session
#
# Return:
# - pty  - pty opened successfully
# - 0    - error happends
#
# Arguments expected are:
# - self    - Object reference
# - cmd     - Command

sub spawn {
    my ( $self, $cmd ) = @_;

## Define variables for tty
    my ( $pid, $pty, $tty, $tty_fd );

## Create a new pseudo terminal, if error opening library happends
    eval "use IO::Pty;";
    return 0 if $@;

## Create new Pty object, return if object not created
    $pty = eval "new IO::Pty;";
    return 0 if !$pty;

## Execute the program in another process.
    unless ( $pid = fork ) {

## Die child process if spawning failed
        die $self->message_failed_fork() unless defined $pid;

## Disassociate process from existing controlling terminal.
        POSIX::setsid or die $self->message_failed_fork();

## Associate process with a new controlling terminal.
        $tty = $pty->slave;
        $pty->make_slave_controlling_terminal();
        $tty_fd = $tty->fileno;
        close $pty;

## Make stdio use the new controlling terminal.
        open STDIN,  "<&$tty_fd" or die $self->message_failed_fork();
        open STDOUT, ">&$tty_fd" or die $self->message_failed_fork();
        open STDERR, ">&STDOUT"  or die $self->message_failed_fork();
        close $tty;

## Execute requested program.
        exec($cmd) or die $self->message_failed_fork();

    } ## end child process

## Return pty
    return ( $pty, $pid );

} ## end sub spawn

##############################################################################
#
# su()
#
# Became super user in opened session
#
# Return:
# - 1            - command executed successfully
# - 0            - error happends
#
# Arguments expected are:
# - self         - Object reference

sub su {
    my ($self) = @_;

## If session not opened, return error
    if ( !$self->{_session} ) {

## Print failed session message
        $self->message_failed_session();

## Return Error
        return 0;
    }

## If session not root, return error
    if ( !$self->{_root} ) {

## Return Error
        return 0;
    }

## Print Session SU message
    $self->message_executing( $self->{_root}->{su},
        $self->get_terminal_timeout() );

## Set passwd
    my $passwd =
      $self->{ARF}->decrypt_passwd( $self->{node}->{host}, $self->{login} );

## If password not specified
    if ( !$passwd or !$passwd->{content} ) {

## Print invalid password message
        $self->{_error} = $self->message_invalid_passwd();

## Return Error
        return 0;
    }

## Set prompt
    my $prompt = $self->{ARF}->get_node_prompt( $self->{node}, $self->{login} );

## If prompt not specified
    if ( !$prompt or !$prompt->{content} ) {

## Print invalid password message
        $self->{_error} = $self->message_invalid_prompt();

## Return Error
        return 0;
    }

## Execute as telnet or ssh
    if ( $self->get_terminal_id() =~ /$terminal_telnet|$terminal_ssh/i ) {
        return $self->su_telnet( $passwd->{content}, $prompt->{content} );
    }

## Set error if invalid terminal specified
    $self->{_error} = $self->message_invalid_terminal();

## Return Error
    return 0;

} ## end sub su

##############################################################################
#
# su_telnet()
#
# Became super user in telnet session
#
# Return:
# - 1            - command executed successfully
# - 0            - error happends
#
# Arguments expected are:
# - self         - Object reference
# - password     - Super user's password
# - prompt       - Login's prompt

sub su_telnet {
    my ( $self, $password, $prompt ) = @_;

## Set super user prompt
    $self->{_session}->prompt("/$prompt/i");

## Get root prompt
    my $wait = $self->{_root}->{prompt};

## Print command and wait for entering password
    $self->{_session}->print( $self->{_root}->{su} );
    $self->{_session}->waitfor("/$wait/i");

## Print password and wait for super user prompt
    $self->{_session}->print($password);
    $self->{_session}->waitfor("/$prompt/i");

## If error happends, return it
    $self->{_error} = $self->{_session}->errmsg;
    return 0 if $self->{_error};

## Return Ok
    return 1;

} ## end sub su_telnet

1;
